Fail fast when a scope-local registration binds its lifetime to an ancestor scope (#1460) - #1488
Merged
Conversation
) When BeginLifetimeScope(tag, configAction) is used and the configuration action registers a component with InstancePerMatchingLifetimeScope targeting a strict ancestor scope tag, Autofac now throws InvalidOperationException at scope-creation time instead of silently leaking instances into the ancestor. Each such child scope creation would resolve and hoist a fresh component instance into the ancestor scope (because the registration is only visible inside the new child's local registry, which gets a new GUID on each BeginLifetimeScope call and so sharing never deduplicates). Those instances accumulate in the ancestor until the ancestor itself is disposed. The guard is placed in CreateScopeRestrictedRegistry, after the configuration action has been applied and the local tracker holds the explicit registrations. It walks the current scope's ancestor chain to collect strict ancestor tags, then checks each locally-registered MatchingScopeLifetime for any tag that appears in that ancestor set. Matching the new scope's own tag or a future descendant tag is still permitted.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1488 +/- ##
===========================================
+ Coverage 77.69% 78.00% +0.30%
===========================================
Files 217 217
Lines 5829 5897 +68
Branches 1253 1265 +12
===========================================
+ Hits 4529 4600 +71
+ Misses 764 759 -5
- Partials 536 538 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- CheckLocalRegistrationsDoNotBindToAncestorScope now defers the HashSet allocation and parent-chain walk until a MatchingScopeLifetime registration is actually found, avoiding all work on the common (no matching-lifetime) child-scope path - Tag issue #1460 in the guard method comment - Rename test to two-segment scenario name (LocalRegistration_BindToGrandchildTag)
This was referenced Jun 17, 2026
This was referenced Jul 10, 2026
This was referenced Jul 17, 2026
This was referenced Jul 28, 2026
This was referenced Aug 4, 2026
This was referenced Aug 13, 2026
deps: Bump the all-dependencies group with 40 updates
ministryofjustice/CFO-DataManagementSystem#126
Closed
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1460. Registering
InstancePerMatchingLifetimeScope("tag")inside a child scope's configuration lambda, where"tag"refers to a strict ancestor scope, caused a memory leak: the registration is local to the child (a fresh registration with a new GUID on every child creation, so instance sharing never hits), but its lifetime attaches to the ancestor — so every child hoists a new instance into the ancestor, and they accumulate until the ancestor is disposed.Per the decision recorded in the issue thread, this configuration is logically contradictory (the lifetime is strictly longer than the registration's reachability) and is never useful, so we now fail fast rather than leak.
Fix
CreateScopeRestrictedRegistrynow inspects the registrations added by the configuration action after they are materialized. For any registration whose lifetime is aMatchingScopeLifetime, if a matched tag belongs to a strict ancestor scope, it throwsInvalidOperationException. This mirrors the existingCheckTagIsUnique/DuplicateTagDetectedguard.Boundaries that remain legal (and are covered by tests):
CreateScopeRestrictedRegistryand is unaffected.The root container tag is always an ancestor, so binding to it from a child config action also throws.
Tests
Adds 5 tests: the exact repro (binding to an ancestor tag throws), own-tag is allowed, future-descendant tag is allowed, root tag throws, and a multi-tag case where one tag is an ancestor throws. Existing matching-scope/lifetime tests pass unchanged.
Adds resource string
MatchingScopeLifetimeAncestorTag(internal). No public API changes. Zero warnings/errors; full suites pass on net8.0 and net10.0.Note: this converts previously-silent (leaking) behavior into a thrown exception. It adds no API and only affects code that was already leaking.